﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Execution
{
    using System;
    using ConnectBasic.Execution.Binding;
    using ConnectBasic.ObjectModel;
    using Microsoft.Linq.Expressions;
    using Microsoft.Scripting;

    internal class BasicBinaryOperationBinder : BinaryOperationBinder
    {
        private readonly BinaryOperationKind mOperationKind;

        public BasicBinaryOperationBinder(BinaryOperationKind operationKind)
            : base(MapBinaryOperationKind(operationKind))
        {
            mOperationKind = operationKind;
        }

        public override DynamicMetaObject FallbackBinaryOperation(DynamicMetaObject target, DynamicMetaObject arg, DynamicMetaObject errorSuggestion)
        {
            var result = this.FallbackBinaryOperationCore(target, arg);

            if (result != null)
            {
                return result;
            }
            else
            {

                // Unknown
                return errorSuggestion ?? DynamicMetaObject.CreateThrow(target, new[] { arg }, typeof(NotSupportedException), (object[])null);
            }
        }

        private DynamicMetaObject FallbackBinaryOperationCore(DynamicMetaObject left, DynamicMetaObject right)
        {
            switch (mOperationKind)
            {
                case BinaryOperationKind.Concatenate:
                    if ((left.Value is string || left.Value == null) && (right.Value is string || right.Value == null))
                    {
                        var leftExpression = left.Expression;
                        var rightExpression = right.Expression;

                        if (leftExpression.Type != typeof(string))
                        {
                            leftExpression = Expression.Convert(leftExpression, typeof(string));
                        }

                        if (rightExpression.Type != typeof(string))
                        {
                            rightExpression = Expression.ConvertChecked(rightExpression, typeof(string));
                        }

                        return new DynamicMetaObject(
                            Expression.Call(
                                typeof(string).GetMethod("Concat", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, new Type[] { typeof(string), typeof(string) }, null),
                                leftExpression,
                                rightExpression),
                                BindingRestrictions.GetExpressionRestriction(
                                    Expression.AndAlso(
                                        Expression.OrElse(
                                            Expression.TypeIs(left.Expression, typeof(string)),
                                            Expression.Equal(left.Expression, Expression.Constant(null))
                                        ),
                                        Expression.OrElse(
                                            Expression.TypeIs(right.Expression, typeof(string)),
                                            Expression.Equal(right.Expression, Expression.Constant(null))
                                        )
                                    )
                                )
                            );
                    }
                    break;

                case BinaryOperationKind.Add:
                    if (left.Value is short && right.Value is short)
                    {
                        return new DynamicMetaObject(
                            Expression.AddChecked(
                                left.Expression,
                                right.Expression),
                                BindingRestrictions.GetTypeRestriction(left.Expression, typeof(short)).Merge(
                                    BindingRestrictions.GetTypeRestriction(right.Expression, typeof(short))
                                )
                        );
                    }
                    break;

                case BinaryOperationKind.Subtract:
                    if (left.Value is short && right.Value is short)
                    {
                        return new DynamicMetaObject(
                            Expression.SubtractChecked(
                                left.Expression,
                                right.Expression),
                                BindingRestrictions.GetTypeRestriction(left.Expression, typeof(short)).Merge(
                                    BindingRestrictions.GetTypeRestriction(right.Expression, typeof(short))
                                )
                        );
                    }
                    break;

                case BinaryOperationKind.LessThan:
                case BinaryOperationKind.LessThanOrEqualTo:
                case BinaryOperationKind.GreaterThan:
                case BinaryOperationKind.GreaterThanOrEqualTo:
                case BinaryOperationKind.EqualTo:
                case BinaryOperationKind.NotEqualTo:
                    return this.FallbackBinaryComparison(left, right);
            }

            // No joy
            return null;
        }

        private DynamicMetaObject FallbackBinaryComparison(DynamicMetaObject left, DynamicMetaObject right)
        {
            var leftExpression = left.Expression;
            var rightExpression = right.Expression;
            BindingRestrictions restrictions = BindingRestrictions.Empty;

            // Check the argument types
            if (left.Value is short && right.Value is short)
            {
                restrictions =
                    BindingRestrictions.GetTypeRestriction(leftExpression, typeof(short)).Merge(
                    BindingRestrictions.GetTypeRestriction(rightExpression, typeof(short)));

                leftExpression = Expression.Convert(leftExpression, typeof(short));
                rightExpression = Expression.Convert(rightExpression, typeof(short));
            }
            else if (left.Value is int && right.Value is int)
            {
                restrictions =
                    BindingRestrictions.GetTypeRestriction(leftExpression, typeof(int)).Merge(
                    BindingRestrictions.GetTypeRestriction(rightExpression, typeof(int)));

                leftExpression = Expression.Convert(leftExpression, typeof(int));
                rightExpression = Expression.Convert(rightExpression, typeof(int));
            }
            else if (left.Value is double && right.Value is double)
            {
                restrictions =
                    BindingRestrictions.GetTypeRestriction(leftExpression, typeof(double)).Merge(
                    BindingRestrictions.GetTypeRestriction(rightExpression, typeof(double)));

                leftExpression = Expression.Convert(leftExpression, typeof(double));
                rightExpression = Expression.Convert(rightExpression, typeof(double));
            }
            else if (left.Value is string && right.Value is string)
            {
                restrictions =
                     BindingRestrictions.GetTypeRestriction(leftExpression, typeof(string)).Merge(
                     BindingRestrictions.GetTypeRestriction(rightExpression, typeof(string)));

                leftExpression = Expression.Call(
                    typeof(string).GetMethod("Compare", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, new Type[] { typeof(string), typeof(string) }, null),
                    Expression.Convert(leftExpression, typeof(string)),
                    Expression.Convert(rightExpression, typeof(string)));

                rightExpression = Expression.Constant(0);
            }
            else
            {
                bool proceed = false;

                // Okay it looks like we need to insert some conversions, by default we should try
                // converting the LHS to the same type as the RHS

                // For COM, see if the COM has a default property we can use                
                if (ComBinder.IsComObject(left.Value))
                {
                    DynamicMetaObject result;

                    if (ComBinder.TryBindGetIndex(new BasicGetIndexBinder(new CallInfo(0)), left, new DynamicMetaObject[0], out result))
                    {

                        // That seemed to work, update the LHS expression to be an expression which gets the default property
                        // of the COM object, and wrap it in a conversion to make sure we've got the right value type 
                        leftExpression = Expression.Dynamic(new BasicConvertBinder(right.RuntimeType), right.RuntimeType, result.Expression);

                        // Merge in restrictions
                        restrictions = restrictions.Merge(result.Restrictions);

                        // Let's see how far we get now
                        proceed = true;
                    }
                }

                // If we didn't find a successful conversion...
                if (!proceed)
                {
                    return null;
                }
            }

            // If we got here, we successfully matched, return the appropriate comparison
            switch (mOperationKind)
            {
                case BinaryOperationKind.LessThan:
                    return new DynamicMetaObject(Expression.LessThan(leftExpression, rightExpression), restrictions);

                case BinaryOperationKind.LessThanOrEqualTo:
                    return new DynamicMetaObject(Expression.LessThanOrEqual(leftExpression, rightExpression), restrictions);

                case BinaryOperationKind.GreaterThan:
                    return new DynamicMetaObject(Expression.GreaterThan(leftExpression, rightExpression), restrictions);

                case BinaryOperationKind.GreaterThanOrEqualTo:
                    return new DynamicMetaObject(Expression.GreaterThanOrEqual(leftExpression, rightExpression), restrictions);

                case BinaryOperationKind.EqualTo:
                    return new DynamicMetaObject(Expression.Equal(leftExpression, rightExpression), restrictions);

                case BinaryOperationKind.NotEqualTo:
                    return new DynamicMetaObject(Expression.NotEqual(leftExpression, rightExpression), restrictions);

                default:

                    // Can't get here
                    throw new InvalidProgramException();
            }
        }

        private static ExpressionType MapBinaryOperationKind(BinaryOperationKind operationKind)
        {

            // NOTE: the .ctor for BinaryOperationBinder is very finicky about
            // what can be passed, so here for example, we can't return the xxxChecked versions
            // of Add, Multiply, or Subtract
            switch (operationKind)
            {
                case BinaryOperationKind.Add:
                    return ExpressionType.Add;

                case BinaryOperationKind.Concatenate:
                    return ExpressionType.Extension;

                case BinaryOperationKind.DivideFloating:
                    return ExpressionType.Divide;

                case BinaryOperationKind.DivideIntegral:
                    return ExpressionType.Extension;

                case BinaryOperationKind.EqualTo:
                    return ExpressionType.Equal;

                case BinaryOperationKind.GreaterThan:
                    return ExpressionType.GreaterThan;

                case BinaryOperationKind.GreaterThanOrEqualTo:
                    return ExpressionType.GreaterThanOrEqual;

                case BinaryOperationKind.LessThan:
                    return ExpressionType.LessThan;

                case BinaryOperationKind.LessThanOrEqualTo:
                    return ExpressionType.LessThanOrEqual;

                case BinaryOperationKind.Multiply:
                    return ExpressionType.Multiply;

                case BinaryOperationKind.NotEqualTo:
                    return ExpressionType.NotEqual;

                case BinaryOperationKind.Power:
                    return ExpressionType.Power;

                case BinaryOperationKind.Subtract:
                    return ExpressionType.Subtract;

                case BinaryOperationKind.RefIsEqual:
                case BinaryOperationKind.RefIsNotEqual:
                default:
                    throw new ArgumentOutOfRangeException("operationKind");
            }
        }
    }
}
